/*
 * Membrane.java
 *
 * Created on Jan 20, 2011, 5:06:46 PM
 *
 * Copyright (c) 2011 Artur Rataj.
 *
 * This code is distributed under the terms of the GNU Library
 * General Public License, either version 2 of the license or, at
 * your option, any later version.
 */

package pl.gliwice.iitis.spatialnn.network;

import java.util.*;

import pl.gliwice.iitis.graphutils.Point;
import pl.gliwice.iitis.graphutils.view.Drawable;

/**
 * A one--dimensional linear membrane. The membrane is symmetric, that
 * is, the beginning and the end are conventional.
 * 
 * @author Artur Rataj
 */
public class Membrane extends Component implements Drawable {
    /**
     * Unique key of this membrane.
     */
    public final String key;
    /**
     * Type of this membrane.
     */
    public MembraneType type;
    /**
     * Location of the beginning of this membrane.
     */
    public  Point locBeg;
    /**
     * Location of the end of this membrane.
     */
    public  Point locEnd;
//    /**
//     * Index of the expression, whose values are spanned on this
//     * membrane.
//     */
//    public final int expressionIndex;
    /**
     * Value of the expression at the beginning.
     */
    public double expressionBeg;
    /**
     * Value of the expression at the end.
     */
    public double expressionEnd;
    /**
     * A list of either inputs or outputs of this membrane, after <code>sync()</code>
     * sorted by expression.<br>
     * 
     * If this membrane's type is <code>IN<code>, then targets, to which this membrane outputs.
     * If this membrane's type is <code>OUT<code>, then targets, which input to this membrane.
     */
    public AbstractTarget[] connections;
    /**
     * Raster resolution of this membrane.
     */
    protected int res;
    /**
     * A precomputed coefficient for <code>e2p</code>.
     */
    protected double e2pMult;
    /**
     * A precomputed coefficient for <code>p2e</code>.
     */
    protected double p2eMult;
    /**
     * Expression of subsequent connections to this membrane, nul for not computed
     * yet. This array is weakly increasing.
     */
    public double[] expressions;
    /**
     * Raster borders for each connection, as defined in <code>computeBorders()</code>.
     * Null for not computed yet.
     */
    public int[] borders;
    
    /**
     * Creates a new instance of Membrane.
     * 
     * @param key unique key of this membrane
     * @param type type of this membrane
     * @param generation generation, must be &lt;0
     * @param locBeg location of the beginning
     * @param locEnd location of the end
     * // param expressionIndex expression index
     * @param expressionBeg expression value at the beginning
     * @param expressionEnd expression value at the end
     */
    public Membrane(String key, MembraneType type,
            Point locBeg, Point locEnd, /* int expressionIndex, */
            double expressionBeg, double expressionEnd) {
        this.key = key;
        this.type = type;
        this.locBeg = locBeg;
        this.locEnd = locEnd;
        /*
        this.expressionIndex = expressionIndex;
        */
        this.expressionBeg = expressionBeg;
        this.expressionEnd = expressionEnd;
        connections = new AbstractTarget[0];
        res = 0;
    }
    /**
     * Adds a connection. See <code>connections</code> for details.
     * 
     * @param target target of the connection to add
     */
    private void addConnection(AbstractTarget target) {
        AbstractTarget[] a = new AbstractTarget[connections.length + 1];
        System.arraycopy(connections, 0, a, 0, connections.length);
        a[connections.length] = target;
        connections = a;
    }
    @Override
    public void addInput(AbstractTarget target) {
        if(type != MembraneType.OUT)
            throw new RuntimeException("can not add an input to a non--output membrane");
        addConnection(target);
    }
    @Override
    public void removeInput(int index) {
        if(type != MembraneType.OUT)
            throw new RuntimeException("can not remove an input from a non--output membrane");
        AbstractTarget[] newInputs = new AbstractTarget[connections.length - 1];
        for(int j = 0; j < newInputs.length; ++j)
            if(j < index)
                newInputs[j] = connections[j];
            else
                newInputs[j] = connections[j + 1];
        connections = newInputs;
    }
    @Override
    public void removeOutputComponent(int index) {
        if(type != MembraneType.IN)
            throw new RuntimeException("can not remove an output component from a non--input membrane");
        AbstractTarget[] newOutput = new AbstractTarget[connections.length - 1];
        for(int j = 0; j < newOutput.length; ++j)
            if(j < index)
                newOutput[j] = connections[j];
            else
                newOutput[j] = connections[j + 1];
        connections = newOutput;
    }
    @Override
    public AbstractTarget[] getInputs() {
        if(type != MembraneType.OUT)
            throw new RuntimeException("can not get inputs from a non--output membrane");
        return connections;
    }
    @Override
    public AbstractTarget[] getOutputComponents() {
        if(type != MembraneType.IN)
            throw new RuntimeException("can not get output components from a non--input membrane");
        return connections;
    }
    /**
     * Adds a neuron to the output.
     * 
     * @param target target of the output neuron to add
     */
    public void addOutputNeuron(AbstractTarget target) {
        if(type != MembraneType.IN)
            throw new RuntimeException("cannot add an output neuron to a non--input membrane");
        addConnection(target);
    }
    /**
     * Returns the angle of this membrane.<br>
     * 
     * Examples: 0.0 for normal at x axis direction, Pi/2 for normal at y axis direction
     * 
     * @return an angle, rangle 0 .. Pi
     */
    public double getAngle() {
        Point diff = locEnd.minus(locBeg);
        double angle = Math.atan2(diff.y, diff.x) + Math.PI/2.0;
        if(angle < 0.0)
            angle += Math.PI;
        else if(angle > Math.PI)
            angle -= Math.PI;
        return angle;
    }
    /**
     * Sets the raster resolution of this membrane.
     * 
     * @param res resolution in pixels, 0 for unspecified
     */
    public void setRes(int res) {
        this.res = res;
        e2pMult = res/(expressionEnd - expressionBeg);
        if(res != 1)
            p2eMult = (expressionEnd - expressionBeg)/(res - 1);
        else
            p2eMult = 0;
    }
    /**
     * Resturns the raster resolution of this membrane.
     * 
     * @return resolution in pixels, 0 for unspecified
     */
    public int getRes() {
        return res;
    }
    /**
     * Converts expression to raster index.
     * 
     * @param expression expression
     * @return raster index, 0 ... <code>res</code> - 1
     */
    public int e2p(double expression) {
        int p = (int)(
                (expression - expressionBeg)*e2pMult);
        if(p == res)
            --p;
        return p;
    }
    /**
     * Converts raster index to expression
     * 
     * @return pixel index, 0 ... <code>res</code> - 1
     * @param expression
     * 
     */
    public double p2e(int pixel) {
        return expressionBeg + pixel*p2eMult;
    }
    /**
     * Returns an expression of a connection, on the end of this membrane.
     * 
     * @param network a network, to which this membrane belongs
     * @param a target, belonging to this mebrane's connections
     * @param inputCount which if the subsequent connections to <code>t</code>
     * fount, to take into account; 0 for the first one etc.
     */
    protected double getExpression(Network network, AbstractTarget t,
            int inputCount) {
        MembraneTarget mC = null;
        /*
        if(t instanceof MembraneTarget) {
            if(type != MembraneType.OUT)
                throw new RuntimeException("membrane type mismatch");
            mC = (MembraneTarget)t;
            if(!mC.key.equals(key))
                throw new RuntimeException("membrane target mismatch");
        } else */ {
            //if(type != MembraneType.IN)
            //    throw new RuntimeException("membrane type mismatch");
            Neuron n = network.neuronMap.get(
                    ((NeuronTarget)t).key);
            AbstractTarget[] targets;
            switch(type) {
                case IN:
                    targets = n.inputs;
                    break;
                    
                case OUT:
                    targets = n.output;
                    break;
                    
                default:
                    throw new RuntimeException("unknown type");
            }
            int count = 0;
            for(AbstractTarget u : targets)
                // a neuron can have inputs to both membranes and other neurons
                if(u instanceof MembraneTarget) {
                    MembraneTarget v = (MembraneTarget)u;
                    if(v.key.equals(key)) {
                        if(count == inputCount) {
                            mC = v;
                            break;
                        } else
                            ++count;
                    }
                }
        }
        if(mC == null)
            mC = mC;
        return mC.expression;
    }
    /**
     * Computes <code>expressions</code>, sorts connections by expression.
     * If the expressions are already computed, this method does nothing.
     * 
     * @param network a network, to which this membrane belongs
     */
    public void computeExpressions(Network network) {
        if(expressions == null) {
            int num = connections.length;
            SortedMap<Double, List<AbstractTarget>> sorted = new TreeMap<>();
            Map<Integer, Integer> inputCount = new HashMap<>();
            for(AbstractTarget t : connections) {
                int key = ((NeuronTarget)t).key;
                Integer ic = inputCount.get(key);
                if(ic == null)
                    ic = 0;
                else
                    ++ic;
                inputCount.put(key, ic);
                double e = getExpression(network, t, ic);
                List<AbstractTarget> list = sorted.get(e);
                if(list == null) {
                    list = new LinkedList<>();
                    sorted.put(e, list);
                }
                list.add(t);
            }
            expressions = new double[num];
            int count = 0;
            for(double e : sorted.keySet()) {
                List<AbstractTarget> list = sorted.get(e);
                for(AbstractTarget t : list) {
                    connections[count] = t;
                    expressions[count] = e;
                    ++count;
                }
            }
        }
    }
    /**
     * Returns raster borders for a given membrane. The borders denote regions of
     * each neural connection. <br>
     * 
     * The field <code>expressions</code> must already be computed.
     * 
     * The format is as follows: an array containing pairs
     * ([-]minimum index, [-]maximum index) defining regions
     * of pixels for each connection, some connections may
     * share a single pixel; a negated index means, that a pixel
     * is shared with adjacent region.
     */
    protected void computeBorders() {
        int num = expressions.length;
        // an even value <code>2*n</code> denotes, that
        // the border is between pixels <code>n - 1</code> and <code>n</code>,
        // where an odd value <code>2*n + 1</code> denotes, that the <code>n</code>th
        // pixel is shared between two adjacent regions.
        int[] b = new int[num + 1];
        int count = 0;
        int prevP = -1;
        for(int i = 0; i < num; ++i) {
            int p = e2p(expressions[i]);
            int q;
            if(prevP == -1)
                q = 0;
            else
                q = prevP + p + 1;
            b[count++] = q;
            prevP = p;
        }
        b[count++] = getRes()*2;
        borders = new int[num*2];
        int pNextPixel = -1;
        for(int i = 0; i < num; ++i) {
            int prevB = b[i + 0];
            int nextB = b[i + 1];
            int prevPixel = prevB/2;
            int nextPixel = (nextB - 1)/2;
            borders[i*2 + 0] = prevPixel;
            borders[i*2 + 1] = nextPixel;
            if(pNextPixel == prevPixel) {
                // mark a common pixel
                borders[i*2 - 1] *= -1;
                borders[i*2 + 0] *= -1;
            }
            pNextPixel = nextPixel;
        }
    }
    @Override
    public void sync(Network network) {
        computeExpressions(network);
        computeBorders();
    }
    /**
     * Propagates a signal through this membrane.<br>
     * 
     * If this is an input membrane, then <code>in</code> contains raster data,
     * and data for subsequent connections is returned.
     * If this is an output membrane, then <code>in</code> contains data for subsequent
     * connections, and raster data is returned.
     * 
     * @param in values of the signal
     * @param preallocatedOut preallocated output array -- for type <code>IN<code>,
     * must have the length of <code>connections.length</code>,
     * for type <code>OUT<code> must have the length of <code>getRes()</code>
     * @return output data
     */
    public double[] propagate(double[] in, double[] preallocatedOut) {
        double[] out;
        int num = connections.length;
        switch(type) {
            case IN:
            {
                if(in.length != getRes())
                    throw new RuntimeException("input length mismatch");
                if(preallocatedOut != null) {
                    out = preallocatedOut;
                    if(out.length != num)
                        throw new RuntimeException("output length mismatch");
                } else
                    out = new double[num];
                for(int i = 0; i < num; ++i) {
                    int prevPixel = borders[i*2 + 0];
                    int nextPixel = borders[i*2 + 1];
                    boolean prevCommon;
                    if(prevCommon = prevPixel < 0)
                        prevPixel *= -1;
                    boolean nextCommon;
                    if(nextCommon = nextPixel < 0)
                        nextPixel *= -1;
                    if(prevPixel == nextPixel)
                        out[i] = in[prevPixel];
                    else {
                        double sum = 0.0;
                        int sumCount = 0;
                        for(int j = prevPixel; j <= nextPixel; ++j) {
                            if(
                                    (prevCommon && j == prevPixel) ||
                                    (nextCommon && j == nextPixel)) {
                                sum += in[j]/2.0;
                                ++sumCount;
                            } else {
                                sum += in[j];
                                sumCount += 2;
                            }
                        }
                        out[i] = 2.0*sum/sumCount;
                    }
                }
                break;
            }
            case OUT:
            {
                if(in.length != num)
                    throw new RuntimeException("input length mismatch");
                int res = getRes();
                if(preallocatedOut != null) {
                    out = preallocatedOut;
                    if(out.length != res)
                        throw new RuntimeException("output length mismatch");
                } else
                    out = new double[res];
                int[] weight;
                final boolean WEIGHTS = true;
                if(WEIGHTS) {
                    weight = new int[res];
                    for(int i = 0; i < res; ++i)
                        out[i] = 0.0;
                } else
                    weight = null;
                for(int i = 0; i < num; ++i) {
                    int prevPixel = borders[i*2 + 0];
                    int nextPixel = borders[i*2 + 1];
                    boolean prevCommon;
                    if(prevCommon = prevPixel < 0)
                        prevPixel *= -1;
                    boolean nextCommon;
                    if(nextCommon = nextPixel < 0)
                        nextPixel *= -1;
                    double value = in[i];
                    for(int j = prevPixel; j <= nextPixel; ++j) {
                        if(WEIGHTS) {
                            out[j] += value;
                            ++weight[j];
                        } else {
                            if(prevCommon && j == prevPixel)
                                out[j] += value/2.0;
                            else if(nextCommon && j == nextPixel)
                                out[j] = value/2.0;
                            else
                                out[j] = value;
                        }
                    }
                }
                if(WEIGHTS)
                    for(int i = 0; i < res; ++i)
                        out[i] = out[i]/weight[i];
                break;
            }
            default:
                throw new RuntimeException("unknown type");
                
        }
        return out;
    }
    @Override
    public String toString() {
        return "membrane " + key;
    }
}
